package com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper;

import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.global.BaseJSConstants.copyArguments;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.getVariableFromStatement;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.isArrayReference;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.isDecl;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.isFunctionReference;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.isObjectReference;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.isValue;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.isVariableDecl;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.readFrom;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.readUntil;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.readUntilEitherWithFeedback;
import static com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.interper.BaseJSInterperUtils.removeTrailingSpaces;

import java.io.StringReader;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.data.Sets.DataSet;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.BaseJSEnvironment;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.BaseJSFunction;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.BaseJSObject;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.BaseJSVariable;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.MissingFunctionError;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.MissingVariableError;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.UnexpectedLengthError;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.UnexpectedObjectTypeError;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.UnknownOperationError;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.evaler.BaseJSEvaluator.VariableType;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.global.BaseJSNativeFunction;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.mods.emath.baseJSMaths;

public class BaseJSInterper {

	private static final Logger log = LoggerFactory.getLogger();

	private static Object processObject(final String line, final String fullLine, final BaseJSEnvironment e, BaseJSVariable object,
			final BaseJSFunction function) {
		if (object == null)
			throw new NullPointerException("object == null!");
		if (object.isVariable()) {
			object = (BaseJSVariable) object.get();
			if (object == null)
				throw new NullPointerException("object == null!");
		}
		final String functionName = readUntil(line, '(');
		String value;
		try {
			final String _values = readFrom(line, '(');
			value = _values.substring(0, _values.length() - 1);
		} catch (Exception n) {
			value = "";
		}
		if (functionName.equals("split")) {
			if (object.isString()) {
				if (value.equalsIgnoreCase("\"\""))
					return StringsUtil.charArrToString(((String) object.get()));
				return ((String) object.get()).split(value);
			} else
				throw new UnknownOperationError("Cannot handle: " + object.get());
		} else if (functionName.equals("reverse")) {
			if (object.isString())
				return StringsUtil.reverse((String) object.get());
			else if (object.isArray()) {
				if (object.get() instanceof String[]) {
					final String[] c = (String[]) object.get();
					ArrayUtils.reverse(c);
					object.set(c);
					return c;
				} else if (object.get() instanceof int[]) {
					final int[] c = (int[]) object.get();
					ArrayUtils.reverse(c);
					object.set(c);
					return c;
				} else if (object.get() instanceof Object[]) {
					final Object[] c = (Object[]) object.get();
					ArrayUtils.reverse(c);
					object.set(c);
					return c;
				}
			}
		} else if (functionName.equals("splice")) {
			if (object.isArray()) {
				final String[] args = Splitters.splitString(StringsUtil.substringBetween(line, "splice(", ")"), ",");
				final int[] d = new int[args.length];
				for (int i = 0; i < d.length; i++) {
					if (StringsUtil.isNumber(args[i])) {
						d[i] = (int) StringsUtil.asNumber(args[i]);
					} else if (function.valueCache.containsKey(args[i])) {
						d[i] = function.valueCache.get(args[i]).tryInt();
					} else {
						throw new UnexpectedObjectTypeError("Unknown object: " + args[i]);
					}
				}
				if (d.length < 2 || d.length > 2)
					throw new UnexpectedLengthError("splice() takes 2 arguments.");
				log.trace("splice() -> from: " + d[0] + ", to: " + d[1] + ", line: " + line);
				if (object.get() instanceof int[]) {
					final int[] f = Arrays.copyOfRange((int[]) object.get(), d[1], ((int[]) object.get()).length);
					object.set(f);
					return f;
				} else {
					final Object[] f = Arrays.copyOfRange((Object[]) object.get(), d[1], ((Object[]) object.get()).length);
					object.set(f);
					return f;
				}
			}
		} else if (functionName.equals("length")) {
			if (object.isString() || object.isArray())
				return object.length();
		} else if (functionName.equals("join")) {
			if (object.isArray()) {
				if (value.equalsIgnoreCase("\"\"") && object.get() instanceof String[])
					return StringsUtil.join((String[]) object.get(), "");
				if (object.get() instanceof String[])
					return StringsUtil.join((String[]) object.get(), value);
			} else if (object.isString()) {
				if (value.equalsIgnoreCase("\"\""))
					return Splitters.splitString(((String) object.get()), "");
				return ((String) object.get()).split(value);
			}
		}
		throw new UnknownOperationError("Unknown process: " + functionName);
	}

	public static Object evalStatement(final String line, final BaseJSEnvironment e, final BaseJSFunction function) {
		if (line.contains("+") || line.contains("*") || line.contains("/") || line.contains("-") || line.contains("%") || line.contains("Math.pow")) {
			final StringReader r = new StringReader(line);
			final List<DataSet<String, Character>> ops = FastLists.list();
			final Map<String, Double> vars = FastMaps.map();
			while (true) {
				final DataSet<String, Character> lhs = readUntilEitherWithFeedback(r, '+', '-', '/', '*', '%');
				if (lhs == null || lhs.getA().isEmpty())
					break;
				ops.add(lhs);
				if (lhs.getB().equals('\uffff'))
					break;
			}
			String query = line;
			for (final DataSet<String, Character> ds : ops) {
				final String i = ds.getA();
				if (function != null && function.valueCache.containsKey(i)) {
					final BaseJSVariable v = function.valueCache.get(i);
					if (!v.isNumber())
						throw new UnknownOperationError("Cannot handle: " + v);
					vars.put(i, (double) StringsUtil.asNumber((Number) v.get()));
				} else if (i.startsWith("return ")) {
					final String op = line.substring(7);
					if (function != null && function.valueCache.containsKey(op)) {
						return function.valueCache.get(op).get();
					} else if (line.contains("+") || line.contains("*") || line.contains("/") || line.contains("-") || line.contains("%")
							|| line.contains("Math.pow(")) {
						return evalStatement(op, e, function);
					} else
						throw new UnsupportedOperationException("Cannot return: " + op);
				} else if (isObjectReference(function, i)) {
					final String f = readFrom(i, '.');
					final String a = readUntil(i, '.');
					if (!function.valueCache.containsKey(a))
						throw new MissingVariableError("Couldn't find: " + a);
					final Object o = processObject(f, i, e, function.valueCache.get(a), function);
					query = query.replace(i, String.valueOf(o));
				} else if (StringsUtil.isNumber(i)) {
					/* NO-OP */
				} else if (i.contains("Math.pow(")) {
					final String pre = readFrom(i, '(');
					final String end = readUntil(pre, ')');
					final String[] vals = end.split(",");
					final Object[] submit = new Object[vals.length];
					copyArguments(vals, submit);
					if (submit.length != 2)
						throw new IllegalArgumentException("submit != 2");
					final double output = Math.pow(((Number) submit[0]).doubleValue(), ((Number) submit[1]).doubleValue());
					query = query.replace(i, String.valueOf(output));
				} else if (StringsUtil.isBlank(i) || i.equalsIgnoreCase(" ")) {
					query = query.replace(i, "");
				} else {
					throw new UnknownOperationError("Cannot handle: " + i);
				}
			}
			return baseJSMaths.eval(query, vars).eval();
		}
		throw new UnexpectedObjectTypeError("Cannot handle: " + line);
	}

	public static void interpretOperation(String line, final BaseJSFunction function, final BaseJSEnvironment e, final Object... variables) {
		/// XXX: Use StringsUtil::stripStart(line)
		while (line.startsWith(" "))
			line = line.substring(1);
		if (isVariableDecl(line)) {
			final String[] left = Splitters.splitString(readUntil(line, '='), " ");
			VariableType type;
			final String _l = ArrayUtils.StringArrayUtils.at(left, 0, "");
			if (_l.equals("const")) {
				type = VariableType.CONSTANT;
			} else if (_l.equals("let")) {
				type = VariableType.LET_CONST;
			} else if (_l.equals("var")) {
				type = VariableType.REGULAR;
			} else {
				log.warn("Unknown variable type: " + _l);
				type = VariableType.REGULAR;
			}
			final String varName = ArrayUtils.StringArrayUtils.at(left, 1, null);
			if (varName == null)
				throw new NullPointerException("[process::isVariableDecl] varName became a nullptr!");
			final String value = readFrom(line, '=');
			if (isArrayReference(value)) {
				if (value.startsWith("[") && value.endsWith("]")) {
					final String arr = value.substring(1, value.length() - 2);
					final String[] values = Splitters.splitString(arr, ",");
					final Object[] objects = new Object[values.length];
					for (int i = 0; i < objects.length; i++) {
						final String _av = values[i];
						final String av = removeTrailingSpaces(_av);
						if (StringsUtil.isNumber(av))
							objects[i] = StringsUtil.asNumber(av);
						else if (av.startsWith("\"") && av.endsWith("\""))
							objects[i] = av.substring(1, av.length() - 2);
						else if (av.startsWith("'") && av.endsWith("'"))
							objects[i] = av.substring(1, av.length() - 2);
						else
							throw new UnsupportedOperationException("Cannot convert: " + av);
					}
					function.valueCache.put(varName, new BaseJSVariable(varName, objects, type));
				} else {
					final String a = readUntil(value, '[');
					if (!function.valueCache.containsKey(a))
						throw new MissingVariableError("Couldn't find array: " + a);
					final String _i = readFrom(value, '[');
					final String i = _i.substring(0, _i.length() - 1);
					final int index = MathUtil.safeParseInt(i, -1);
					if (index == -1)
						throw new UnknownOperationError("Cannot use index: " + i);
					BaseJSVariable _v = function.valueCache.get(a);
					if (_v.isVariable())
						_v = (BaseJSVariable) _v.get();
					if (!_v.isArray())
						throw new UnexpectedObjectTypeError("Cannot handle a non-array: " + a);
					final Object v = Array.get(_v.get(), index);
					final BaseJSVariable p = new BaseJSVariable(varName, v, type);
					function.valueCache.put(varName, p);
				}
			} else if (isValue(value)) {
				Object trueValue = null;
				if (StringsUtil.isNumber(value))
					trueValue = StringsUtil.asNumber(value);
				else if (value.startsWith("\"") && value.endsWith("\"")) {
					trueValue = value.substring(1, value.length() - 2);
				} else if (value.startsWith("'") && value.endsWith("'")) {
					trueValue = value.substring(1, value.length() - 2);
				} else
					throw new UnsupportedOperationException("Unknown value: " + value);
				final BaseJSVariable defV = new BaseJSVariable(varName, trueValue, type);
				function.valueCache.put(varName, defV);
			} else {
				throw new UnknownOperationError("Cannot handle value: " + value);
			}
		} else if (isDecl(line)) {
			final String variable = readUntil(line, '=');
			final String value = readFrom(line, '=');
			if (isObjectReference(function, value)) {
				final String object = readUntil(value, '.');
				final String ref = readFrom(value, '.');
				final Object o = processObject(ref, value, e, function.valueCache.get(object), function);
				function.valueCache.get(object).set(o);
			} else {
				Object r = null;
				if (isArrayReference(value)) {
					final String v = readUntil(value, '[');
					if (!function.valueCache.containsKey(v))
						throw new MissingVariableError("Couldn't find: " + v);
					final String _o = readFrom(value, '[');
					final String o = _o.substring(0, _o.length() - 1);
					if (StringsUtil.isNumber(o)) {
						throw new UnknownOperationError("[interpretOperation::isDecl::isArrayReference::isNumber] Cannot handle: " + o);
					} else {
						r = evalStatement(o, e, function);
					}
				} else if (function.valueCache.containsKey(value)) {
					final BaseJSVariable s = function.valueCache.get(value);
					r = s.get();
				} else if (isValue(value)) {
					Object trueValue = null;
					if (StringsUtil.isNumber(value))
						trueValue = StringsUtil.asNumber(value);
					else if (value.startsWith("\"") && value.endsWith("\"")) {
						trueValue = value.substring(1, value.length() - 2);
					} else if (value.startsWith("'") && value.endsWith("'")) {
						trueValue = value.substring(1, value.length() - 2);
					} else
						throw new UnsupportedOperationException("Unknown value: " + value);
					final BaseJSVariable defV = new BaseJSVariable(variable, trueValue);
					function.valueCache.put(variable, defV);
					return;
				}
				if (r == null)
					throw new NullPointerException("[interpretOperation::isDecl::isArrayReference] No value was given!\n" + "Failed at: " + line);
				Object loc = null;
				if (isArrayReference(variable)) {
					final String v = readUntil(variable, '[');
					if (!function.valueCache.containsKey(v))
						throw new MissingVariableError("Couldn't find: " + v);
					final String _o = readFrom(variable, '[');
					final String o = _o.substring(0, _o.length() - 1);
					if (StringsUtil.isNumber(o)) {
						loc = StringsUtil.asNumber(o);
					} else {
						loc = evalStatement(o, e, function);
					}
				}
				if (loc == null)
					throw new NullPointerException("[interpretOperation::isDecl::isArrayReference] No value was given!\n" + "Failed at: " + line);
				final String seek = getVariableFromStatement(variable);
				if (seek == null)
					throw new UnsupportedOperationException("Unknown variable: " + variable);
				if (!function.valueCache.containsKey(seek))
					throw new MissingVariableError("Missing variable: " + seek);
				BaseJSVariable p = function.valueCache.get(seek);
				if (p.isVariable())
					p = (BaseJSVariable) p.get();
				if (Variables.isNumber(loc) && p.isArray()) {
					int index = -1;
					if (loc instanceof Integer)
						index = (int) loc;
					else if (loc instanceof Double)
						index = (int) (double) loc;
					else
						throw new UnsupportedOperationException("Cannot handle location: " + loc);
					if (String.valueOf(r).endsWith(".0"))
						r = (int) (double) r;
					if (Variables.isNumber(r)) {
						Array.set(p.get(), index, Array.get(p.get(), (int) r));
					} else {
						Array.set(p.get(), index, r);
					}
				} else {
					throw new UnsupportedOperationException("Unknown behaviour for: variable: " + seek + ", value: " + r + ", loc: " + loc);
				}
			}
			// first handle value
			// then handle the variable
			// add isArrayReference(String)
			// add isObjectReference(String)
			// add getInternalFunction()
		} else if (isFunctionReference(line)) {
			if (line.contains(".")) { // FIXME: get .func().func() working
				final String obj = removeTrailingSpaces(readUntil(line, '.'));
				if (!function.hasObject(obj, e)) {
					throw new MissingFunctionError("Missing variable: " + obj);
				}
				final String func = readUntil(readFrom(line, '.'), '(');
				final String _values = readFrom(line, '(');
				final String[] values = Splitters.splitString(_values.substring(0, _values.length() - 1), ",");
				final Object[] vars = new Object[values.length];
				copyArguments(values, vars, function);
				if (function.valueCache.containsKey(obj)) {
					final String object = readUntil(line, '.');
					final String ref = readFrom(line, '.');
					final Object o = processObject(ref, line, e, function.valueCache.get(object), function);
					function.valueCache.get(object).set(o);
				} else {
					BaseJSObject _o = function.getObject(obj, e);
					if (_o instanceof BaseJSVariable)
						_o = (BaseJSObject) ((BaseJSVariable) _o).get();
					if (!_o.contains(func))
						throw new MissingFunctionError(func + " wasn't found in: " + _o);
					final BaseJSObject o = _o.get(func);
					if (!o.isFunction())
						throw new MissingFunctionError(obj + " isn't a function!");
					if (o.isNativeFunction()) {
						((BaseJSNativeFunction) o).run(e.isUsingNewInterpreter(), e, vars);
					} else {
						((BaseJSFunction) o).run(e.isUsingNewInterpreter(), e, vars);
					}
				}
			} else if (line.contains("[") && line.contains("](")) {
				final String obj = removeTrailingSpaces(readUntil(line, '['));
				if (!function.hasObject(obj, e)) {
					throw new MissingFunctionError("Missing variable: " + obj);
				}
				final String func = readUntil(readFrom(line, '['), ']');
				final String _values = readFrom(line, '(');
				final String[] values = Splitters.splitString(_values.substring(0, _values.length() - 1), ",");
				final Object[] vars = new Object[values.length];
				copyArguments(values, vars, function);
				if (function.valueCache.containsKey(obj)) {
					final String object = readUntil(line, '.');
					final String ref = readFrom(line, '.');
					final Object o = processObject(ref, line, e, function.valueCache.get(object), function);
					function.valueCache.get(object).set(o);
				} else {
					BaseJSObject _o = function.getObject(obj, e);
					if (_o instanceof BaseJSVariable)
						_o = (BaseJSObject) ((BaseJSVariable) _o).get();
					if (!_o.contains(func))
						throw new MissingFunctionError(func + " wasn't found in: " + _o);
					final BaseJSObject o = _o.get(func);
					if (!o.isFunction())
						throw new MissingFunctionError(obj + " isn't a function!");
					if (o.isNativeFunction()) {
						((BaseJSNativeFunction) o).run(e.isUsingNewInterpreter(), e, vars);
					} else {
						((BaseJSFunction) o).run(e.isUsingNewInterpreter(), e, vars);
					}
				}
			} else
				throw new UnsupportedOperationException("Unknown function: " + line);
		} else if (line.startsWith("return ")) {
			final String op = line.substring(7);
			if (isFunctionReference(op)) {
				final String obj = readUntil(op, '.');
				if (!function.hasObject(obj, e)) {
					throw new MissingFunctionError("Missing variable: " + obj);
				}
				final String func = readUntil(readFrom(op, '.'), '(');
				final String _values = readFrom(op, '(');
				final String[] values = Splitters.splitString(_values.substring(0, _values.length() - 1), ",");
				final Object[] vars = new Object[values.length];
				copyArguments(values, vars, function);
				if (function.valueCache.containsKey(obj)) {
					final String object = readUntil(op, '.');
					final String ref = readFrom(op, '.');
					final Object o = processObject(ref, op, e, function.valueCache.get(object), function);
					function.returnValue.lazySet(o);
				} else {
					BaseJSObject _o = function.getObject(obj, e);
					if (_o instanceof BaseJSVariable)
						_o = (BaseJSObject) ((BaseJSVariable) _o).get();
					if (!_o.contains(func))
						throw new MissingFunctionError(func + " wasn't found in: " + _o);
					final BaseJSObject o = _o.get(func);
					if (!o.isFunction())
						throw new MissingFunctionError(obj + " isn't a function!");
					if (o.isNativeFunction()) {
						function.returnValue.lazySet(((BaseJSNativeFunction) o).run(e.isUsingNewInterpreter(), e, vars));
					} else {
						function.returnValue.lazySet(((BaseJSFunction) o).run(e.isUsingNewInterpreter(), e, vars));
					}
				}
			} else if (line.contains("+") || line.contains("*") || line.contains("/") || line.contains("-") || line.contains("%")) {
				function.returnValue.lazySet(evalStatement(line, e, function));
			} else if (line.contains("[") && line.endsWith("]")) {
				final String var = readUntil(op, '[');
				if (!function.valueCache.containsKey(var))
					throw new MissingVariableError("Cannot find variable: " + var);
				final String _index = readUntil(readFrom(op, '['), ']');
				int index;
				if (StringsUtil.isNumber(_index))
					index = Integer.parseInt(_index) - 1;
				else
					throw new UnsupportedOperationException("Cannot index-ify: " + _index);
				final BaseJSVariable v = function.valueCache.get(var);
				if (!v.isArray())
					throw new UnsupportedOperationException("Object is not an array!");
				function.returnValue.lazySet(Array.get(v.get(), index));
			} else {
				throw new UnsupportedOperationException("Cannot return: " + op);
			}
		} else {
			log.warn("Unknown operation: " + line);
		}
	}
}
